<?php
declare(strict_types=1);
namespace qhweb\phpsm\ecc\sm2;

use Mdanter\Ecc\Math\GmpMathInterface;
use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface;
use Mdanter\Ecc\Crypto\Key\PublicKeyInterface;
use Mdanter\Ecc\Util\BinaryString;
use Mdanter\Ecc\Crypto\Signature\SignatureInterface;
use Mdanter\Ecc\Crypto\Signature\Signature;
/**
 * SM2 数字签名算法实现类
 *
 * 本类实现了 SM2 椭圆曲线数字签名算法（ECDSA 变种）的签名生成（sign）与签名验证（verify）功能。
 * 基于 Mdanter\Ecc 提供的椭圆曲线数学库，使用 GMP 大整数运算。
 * 
 * 主要用途：
 * - 为给定的私钥与消息哈希生成符合 SM2 规范的数字签名（r, s）。
 * - 验证给定的公钥、签名及消息哈希是否匹配，判断签名是否合法。
 *
 * 注意：
 * - 本类假设传入的 truncatedHash 已为根据 SM2 规范（通常为 Z + msg 的哈希）截断后的哈希值。
 * - 签名过程中会进行多次尝试以防止 r 或 s 为 0 的非法情况，超过最大尝试次数将抛出异常。
 */
class Sm2Signer {

    /**
    * 数学运算适配器（基于 GMP）
    * @var GmpMathInterface
    */
    private $adapter;

    /**
    * 构造方法
    * @param GmpMathInterface $adapter 数学运算适配器实例
    */

    public function __construct( GmpMathInterface $adapter ) {
        $this->adapter = $adapter;
    }

    /**
     * 生成 SM2 数字签名
     *
     * 实现流程遵循 SM2 签名标准（类似 ECDSA），包括：
     * 1. 使用随机数 k 计算椭圆曲线点 P = kG
     * 2. 计算 r = (truncatedHash + P.x) mod n
     * 3. 计算 s = ((1 + d)^-1 * (k - r*d)) mod n
     * 4. 若 r 或 s 为 0，则重试（最多 5 次，超过则抛出异常）
     *
     * @param PrivateKeyInterface $key           私钥对象，包含私钥秘密值及对应椭圆曲线点
     * @param \GMP                $truncatedHash 消息的截断哈希值（通常为 Z + msg 的 SM3 哈希）
     * @param \GMP                $randomK       用于签名的随机数 k（通常应来自 CSPRNG）
     * @return SignatureInterface                 符合规范的签名对象，包含 r 和 s
     * @throws \RuntimeException                  当 r 或 s 为 0 或超过最大尝试次数时抛出
     */

    public function sign( PrivateKeyInterface $key, \GMP $truncatedHash, \GMP $randomK ): SignatureInterface {
        $math = $this->adapter;
        $generator = $key->getPoint();
        // var_dump($generator);die();
        $n = $generator->getOrder();
        $modMath = $math->getModularArithmetic( $n );
        $prikey = $key->getSecret();
        //第一二步是userid, msg 生成 trucatedhash ,
        $count = 0;
        while (true) {
            $count++;
            // echo "count: $count\n";
            if($count >5){
                throw new \RuntimeException( 'Error: sign R or S = 0' );
            }
            // 第三步生成随机数
            $k = $math->mod( $randomK, $n );
            // 第四步 计算pt1(x1,y1) = [K]G这个点
            // 生成一个新的点P = kG
            $p1 = $generator->mul( $k );
            // var_dump($p1);die();
            // 第五步 计算 r = (truncatedHash + x1) mod n
            $r = $modMath->add($truncatedHash,$p1->getX());
            // var_dump(gmp_strval($r,16));die();
            $zero = gmp_init( 0, 10 );
            if ( $math->equals( $r, $zero ) ) {
                // @todo 如报错，重来 
                // continue; //报错重来一次  
                // @todo
                throw new \RuntimeException( 'Error: random number R = 0' );
            }
            // 第六步 计算 s = ((1 + d)^-1 * (k - rd)) mod n
            
            $one = gmp_init(1,10);
            $s1 = $math->inverseMod($math->add($one, $prikey),$n );
            // print_r(gmp_strval($s1,16));die();
            $s2 = $math->sub($k,$math->mul($r,$prikey));
            // print_r(gmp_strval($s2,16));die();
            $s = $modMath->mul($s1,$s2);
            // var_dump($generator->mul($s));die();
            if ( $math->equals( $s, $zero ) ) {
                // continue;
                throw new \RuntimeException( 'Error: random number S = 0' );
            }
            return new Signature( $r, $s );
        }
    }

    /**
     * 验证 SM2 数字签名
     *
     * 验证流程遵循 SM2 签名标准，包括：
     * 1. 检查 r 和 s 是否在 [1, n-1] 范围内
     * 2. 计算 t = (r + s) mod n
     * 3. 计算点 P1 = sG 与 P2 = t * PA（PA 为公钥点）
     * 4. 计算联合点 xy = P1 + P2
     * 5. 计算 v = (e + x1) mod n，判断是否等于 r
     *
     * @param PublicKeyInterface  $key       公钥对象，包含公钥点及生成器
     * @param SignatureInterface  $signature 签名对象，包含 r 和 s
     * @param \GMP                $hash      消息的哈希值（通常为 Z + msg 的 SM3 哈希）
     * @return bool                         签名是否合法（true/false）
     */

    public function verify( PublicKeyInterface $key, SignatureInterface $signature, \GMP $hash ): bool {

        $generator = $key->getGenerator();
        // var_dump($generator);die();
        $n = $generator->getOrder();
        $r = $signature->getR();
        $s = $signature->getS();
        $math = $this->adapter;
        $one = gmp_init( 1, 10 );
        if ( $math->cmp( $r, $one ) < 0 || $math->cmp( $r, $math->sub( $n, $one ) ) > 0 ) {
            return false;
        }

        if ( $math->cmp( $s, $one ) < 0 || $math->cmp( $s, $math->sub( $n, $one ) ) > 0 ) {
            return false;
        }
        
        // 1.2.3.4 sm3 取msg,userid的 hash值,这里直接就传过来了，
        $modMath = $math->getModularArithmetic( $n );
        // 第五步 计算t=(r'+s')mod n
        $t = $modMath->add($r,$s);    
        // // 第六步 计算(x1,y1) = [s]G + [t]PA
        $p1 = $generator->mul($s); // p1 = sG 是OK的与签名生成的sG一样
        $p2 = $key->getPoint()->mul($t);
        $xy = $p1->add($p2);
        // // 第七步 R=(e' + x1') 验证R==r'?       
        $v = $modMath->add($hash, $xy->getX());

        return BinaryString::constantTimeCompare( $math->toString( $v ), $math->toString( $r ) );
    }
}